<?php

/*
  http://code.google.com/p/securestring

  The MIT License

  Copyright (c) 2007, 2010 Nick Galbreath, nickg@client9.com

  Permission is hereby granted, free of charge, to any person obtaining a copy
  of this software and associated documentation files (the "Software"), to deal
  in the Software without restriction, including without limitation the rights
  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  copies of the Software, and to permit persons to whom the Software is
  furnished to do so, subject to the following conditions:

  The above copyright notice and this permission notice shall be included in
  all copies or substantial portions of the Software.

  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  THE SOFTWARE.
*/

/**
 * Design Notes:
 *  This interface is somewhat low-level, as higher-level functions will be
 *  a matter of taste.
 *
 * Input payload is assumed to be a valid "query string", with proper
 * urlencoding.  This is standard with lot of built-in functions to help
 * generation.
 *
 * Once validated, you use standard query string decomposition functions.
 * You'll have some meta data keys starting with "_" (e.g. _mac, _ver)
 * which can be used for policy, expiration, or can be stripped out.
 *
 * salt bytes must be passed in instead of being autogenerated.
 *   Some installations may not wish to have any code using mt_rand and
 *   other built-in php functions for randomness.  In addition, some
 *   installations have their own mechanisms for generating random bytes
 *
 * key array is intentionally simple.
 *   True key management is... uhhh... hard.
 *
 */

/** LOOKING FOR
 *   HELP WITH PEAR-ificiation
 *   php-doc improvements
 *   Translations to other languages
 **/

class SecureString1 {

    /*
     * adjust as needed.  ONLY use one of the three listed below.
     *
     * While there are other choices in
     * http://us.php.net/manual/en/function.hash-algos.php and some
     * are fine, many others are NOT acceptable.  So make it easy on
     * yourself and just pick one of thee.  There is no need to use
     * anything else in the near term.  So why isn't this configurable
     * or data driven?  Really, how often does a hash get busted open?
     * A small mater of programming or use of the version field can be
     * used for emergency upgrades if needed.
     */

    //const DEFAULT_HASH = 'sha1';         /* 28 chars */
    const DEFAULT_HASH = 'sha256';     /* 44 chars */
    //const DEFAULT_HASH = 'sha512';     /* 88 chars */

    // meta data field names
    const PREFIX_CREATED = '_now';
    const PREFIX_VERSION = '_ver';
    const PREFIX_KEYID   = '_kid';
    const PREFIX_SALT    = '_slt';
    const PREFIX_MAC     = '_mac';

    // Minimums Maximums
    const MIN_SALT_LEN   = 4;
    const MIN_KEY_LEN    = 8;

    // PHP integer handling is uhhh, unique
    // overflow is php version and os-dependant
    // http://stackoverflow.com/questions/300840/force-php-integer-overflow
    // Put in hard limit
    const INT_MAX = 2147483647;

    /**
     * Given an input string, add an HMAC
     *
     * @param string $str        input data
     * @param string $salt       salt of random characters
     *                           using base 64 characters
     * @param array $key_array   array of key_id => key
     * @param int $kid           key_id to use
     * @param int $now           default 0 for current time
     * @param int $version       default 1
     * @param int $salt          default 0 for random digits
     *
     * @return the new string if ok, throw Exception if bad input
     */
    static function create($str, $salt, $key_array, $kid, /* required */
                           $now=0, $version=1             /* optional */ ) {

        // make sure 'now', 'version', 'salt', and 'kid' are all integers.
        // by doing so, invalid inputs can not break formating
        // for example contain '&' or '<' which may bust HTML/XML/JS.

        $kid = intval($kid);
        if ($kid <= 0 || $kid >= self::INT_MAX) {
            throw new InvalidArgumentException('key id (kid) must be positive integer');
        }

        // if exactly ZERO, then use current time, else it better be
        //  a positive number
        if ($now === 0) {
            $now = time();
        } else {
            $now = intval($now);
            if ($now <= 0 || $now >= self::INT_MAX) {
                throw new InvalidArgumentException('Timestamp must be positive integer.');
            }
        }

        $version = intval($version);
        if ($version <= 0 || $version >= self::INT_MAX) {
            throw new InvalidArgumentException('Version must be positive integer.');
        }

        $salt = strval($salt);
        if (strlen($salt) < self::MIN_SALT_LEN || ! self::b64_chars($salt)) {
            throw new InvalidArgumentException('Salt must be at least 4 chars made from b64 chars');
        }

        // force it to be string
        $str = strval($str);
        if (strlen($str) === 0) {
            // this should truly never happen
            throw new InvalidArgumentException('Invalid payload, must be non-empty string');
        }

        if (count($key_array) === 0) {
            throw new InvalidArgumentException('Key array has no keys!');
        }

        if (! isset($key_array[$kid])) {
            throw new InvalidArgumentException('Secret key "$kid" does not exist.');
        }
        if (strlen($key_array[$kid]) < self::MIN_KEY_LEN) {
            throw new InvalidArgumentException('Secret key "$kid" is less than 16 chars!');
        }

        // note: we are also hashing _all_ of the meta data
        $payload = $str . '&' .
            self::PREFIX_CREATED . '=' . $now     . '&' .
            self::PREFIX_SALT    . '=' . $salt    . '&' .
            self::PREFIX_KEYID   . '=' . $kid     . '&' .
            self::PREFIX_VERSION . '=' . $version . '&' .
            self::PREFIX_MAC     . '=';

        // make the HMAC with raw binary output
        $hmac = hash_hmac(self::DEFAULT_HASH, $payload, $key_array[$kid], true);
        $hmac = self::b64_urlencode($hmac);

        return $payload . $hmac;
    }

    /**
     * Validate the HMAC on a 'secure string'
     *
     * @param string str the input string
     * @param array key_array, key database
     * @return boolean true if validated, untampered, false otherwise
     */
    static function validate($str, $key_array) {
        // better be a string here
        if (! is_string($str)) {
            return false;
        }


        // Ok, you are right.  The following bits of
        // code could be done using parse_str.  But, parse_str
        // is a very non-trivial function and can generate a large
        // number of temporary strings that aren't useful.
        // The version below also lends itself to translation to other
        // languages.


        // NOTE: that's strRpos, since we expect it to be at the end
        // and we want to get the LAST occurance of it.
        //  i.e.  _ver=1&........&_ver=2   we want the  _ver=2

        // Pull out version
        $prefix = '&' . self::PREFIX_VERSION . '=';
        $kstart = strrpos($str, $prefix);
        if ($kstart === false) {
            return false;
        }
        $kstart += strlen($prefix);
        $kend = strpos($str, '&', $kstart);
        if ($kend === false) {
            return false;
        }
        $ver = substr($str, $kstart, $kend-$kstart);

        // possibly dispatch on version now

        // pull out mac, and original data
        $prefix = '&' . self::PREFIX_MAC . '=';
        $kstart = strrpos($str, $prefix);
        if ($kstart === false) {
            return false;
        }
        // TBD, if kstart is more than 44 chars from end
        //  then this is clearly corrupted, and should
        //  return false

        $kstart += strlen($prefix);
        $payload = substr($str, 0, $kstart);
        $given_mac = substr($str, $kstart);

        // Now find key id in payload
        $prefix = '&' . self::PREFIX_KEYID . '=';
        $kstart = strrpos($payload, $prefix);
        if ($kstart === false) {
            return false;
        }
        $kstart += strlen($prefix);
        $kend = strpos($payload, '&', $kstart);
        if ($kend === false) {
            return false;
        }
        $kid = substr($payload, $kstart, $kend-$kstart);
        if (! isset($key_array[$kid])) {
            return false;
        }

        // recompute hmac, with raw binary output
        $computed = hash_hmac(self::DEFAULT_HASH, $payload,
                              $key_array[$kid], true);
        $computed = self::b64_urlencode($computed);

        // match or die
        // to prevent timing attacks, we use a slow equals version
        // NO
        //return ($computed === $given_mac);
        return self::string_equals($computed, $given_mac);
    }

    /**
     * Web/URL safe base64 encoder
     *
     * There is no standard for "web safe base 64", but this is what
     * I've been using for years in other products via
     * http://code.google.com/p/stringencoders
     * (which is easily used tens of billions of times per day in live
     *  production environments)
     *
     * @param string $str input string
     * @return encoded string that is safe to put in URLs
     */
    static function b64_urlencode($str) {
        return str_replace(array('+','/','='),
                           array('-','_','.'),
                           base64_encode($str));
    }

    /**
     * Web/URL safe base64 decoder
     *
     * @param string $str input encoded string
     * @return decoded string
     */
    static function b64_urldecode($str) {
        return base64_decode(str_replace(array('-','_','.'),
                                         array('+','/','='), $str));
    }

    static function b64_chars($str) {
        return (1 == preg_match( '/^[A-Za-z0-9-_.]*$/', $str));
    }

    /**
     *  Check if two strings are equal using branch-free methods.
     *
     *  This checks every char of two strings for equality and
     *  does not fail fast.  No branching (conditionals) is done in main loop.
     *
     *  This prevents timing attacks where the other string can be guessed
     *  by seeing how long a function runs.  This function does leak the
     *  string length, but that's ok.
     *
     */
    static function string_equals($a, $b) {
        if (strlen($a) != strlen($b)) {
            return false;
        }

        // NOT using 'ord' produces non-deterministic results!
        //  who knows what php is doing.
        $eq = 0;
        for ($i = strlen($a) - 1; $i >= 0; --$i) {
            $eq |= (ord($a[$i]) ^ ord($b[$i]));
        }
        return $eq === 0;
    }
}
?>